脚本化全流程
上一篇给的是原生命令:每一步都看得见,出问题知道卡在哪。代价是每次要手敲十几条、还要反复输密码。本篇把整条链路固化成 autodl/ 下的 7 个脚本——上传解压 → 装环境 → 起训练 → 自动关机 → 取回模型,把所有容易出错的环节(密码复用、字节核对、进程检测、断点续传)都封在脚本里。
9.1 七个脚本各管一段
| 脚本 | 在哪跑 | 负责什么 | 关键设计 |
|---|---|---|---|
deploy.sh | Mac | 上传代码包与脚本(可选数据集)→ 远端解压 → 校验 | SSH 连接复用,全程只输一次密码;每个文件核对远端字节数;解压时静音 macOS 打包留下的 xattr 警告 |
setup.sh | 云端 | 8 步装环境并自检 | 任何一步不达标立刻退出并说明原因,不带着坏环境往下跑;末尾校验数据集条数与帧数 |
wandb_login.sh | 云端(被 setup.sh source) | 导出 WANDB_API_KEY 并写进 ~/.bashrc | 新版 86 字符 key 走 wandb login 会被 SDK 的「必须 40 字符」硬校验拒掉,所以只走环境变量 |
train_act_so101.sh | 云端 | 后台启动 ACT 训练 | 先查 WANDB_API_KEY,缺了立刻失败而不是让训练静默崩;起完 5 秒复查进程,失败直接打日志尾 |
auto_shutdown.sh | 云端 | 训练结束 → 延迟 60 秒 → 关机 | 三重安全:等不到训练绝不关机;DRY_RUN=1 可演练;四种关机路径逐个兜底 |
check_remote.sh | Mac | 一条命令看全远端状态 | checkpoint 列表 / 训练进程 / 日志尾 / 关机守护日志,同时落盘 _remote_check.txt |
fetch_checkpoints.sh | Mac | 把各步 checkpoint 的模型拉回本地 | .done 标记做断点续传;拉完比对字节数;只拉 206MB 的模型,不拉 412MB 的续训状态 |
9.2 全流程:三段命令
第 1 段 · 打包与上传(Mac 上执行):
cd /Users/xxx/project/lerebot
# ① 打包。包内顶层名必须是 lerobot-main/ 和 so101_demo/,云端脚本按这两个名字找路径。
# --no-xattrs 必加:否则 macOS 自带 tar 会把扩展属性写进包,远端解压刷几百行警告。
# -s 只改「包内」的名字,不动磁盘上的目录(lerobot/ 可能正被编辑器占用,别用 mv 改名)。
tar --no-xattrs -s '/^lerobot/lerobot-main/' -czf autodl/lerobot-main.tar.gz lerobot
tar --no-xattrs -czf autodl/so101_demo.tar.gz -C datasets so101_demo
# ② 上传 + 解压 + 校验。-d 表示连数据集一起传(只改代码时去掉它,几百 KB 秒传)
cd autodl
./deploy.sh -h connect.westd.seetacloud.com -P 42143 -u root -d- 地址与端口从 AutoDL 控制台「登录指令」抄。无卡模式和正常模式开机后都可能变,连不上先核对这里。
- 想彻底不输密码,先配一次公钥:
ssh-copy-id -p 42143 root@connect.westd.seetacloud.com。 - 脚本结尾会故意保留复用连接 600 秒,所以你紧接着
ssh上去跑setup.sh时不用再输一遍密码。
第 2 段 · 装环境 + 起训练 + 挂守护(ssh 登进云端后按顺序执行):
ssh -p 42143 root@connect.westd.seetacloud.com
cd /root/autodl-tmp
# ① 装环境:GPU → conda 源 → torch 版本 → lerobot → torchcodec → HF 镜像 → wandb → 数据集自检
bash setup.sh
# ② 让本终端也拿到刚写进 ~/.bashrc 的变量(setup.sh 是子 shell,export 传不回来)
source ~/.bashrc
# ③ 起训练(后台跑,日志写 /root/autodl-tmp/so101_train.log)
bash train_act_so101.sh
# 成功的标志:打印「训练已启动 (PID …)」
# ④ 确认训练真在跑之后,再挂关机守护(训练完自动断电,少跑一小时省一小时)
setsid nohup bash auto_shutdown.sh >> /root/autodl-tmp/auto_shutdown.log 2>&1 < /dev/null &
cat /root/autodl-tmp/auto_shutdown.log
# 成功的标志:出现「检测到 'lerobot-train' 运行中, 开始守护」第 3 段 · 看状态与取回模型(回到 Mac 执行):
cd /Users/xxx/project/lerebot
# 一条命令看全:时间 / checkpoint 列表 / 训练进程 / 日志尾 / 关机守护日志
# 用法与 deploy.sh / fetch_checkpoints.sh 一致:-h 地址 -P 端口 [-u root]
bash autodl/check_remote.sh -h connect.westd.seetacloud.com -P 42143
# 训练结束(或中途)把模型拉回本地,落成 models/<步数>/pretrained_model/
bash autodl/fetch_checkpoints.sh -h connect.westd.seetacloud.com -P 42143 -u rootfetch_checkpoints.sh可以反复跑:已完成的靠.done标记跳过,中断后重跑即可。它不按「文件是否存在」判断完成度——那样会把下到一半的半截文件当成已完成而永久跳过。- 只要模型(评测用)到此为止;要接着续训才需要
training_state,那个 412MB/个,脚本默认不拉。 - 拉回来直接用于评测:
--policy.path=/Users/xxx/project/lerebot/models/025000/pretrained_model(参数细节见上一篇)。
9.3 三个最容易踩的点
会一直不关机:
auto_shutdown.sh用pgrep -f lerobot-train匹配整条命令行。所以任何命令行里含这个字符串的常驻进程都会让它以为训练还没结束,于是永远不关机。最典型的就是这种写法:bashtail -f /root/autodl-tmp/so101_train.log | grep lerobot-train # 千万别挂着看日志用
tail -f就行,别把lerobot-train当grep的参数。真要确认进程,用pgrep -af lerobot-train,或ps -eo pid,cmd | grep '[l]erobot-train'([l]是让grep自己不匹配自己,这个不会拖住守护,因为它不是常驻进程)。
写进 ~/.bashrc ≠ 当前终端生效:
setup.sh会把HF_ENDPOINT写进~/.bashrc,但写文件只对以后新开的终端有效,当前进程读不到;而bash setup.sh又是子 shell,脚本里的export也传不回你的终端。脚本里的处理是两手都抓:写完后当场
export(保证脚本自身后续步骤能用),末尾再按运行方式打印提示。你手动补一条source ~/.bashrc就对了——这正是第 2 段第 ② 步存在的原因,WANDB_API_KEY同理。
为什么到处都在核对字节数:
scp只保证「传了就传了」,中断时可能留下一个看起来正常、实际少了一半的文件。deploy.sh和fetch_checkpoints.sh都在传完后用stat对比字节数,不一致就删掉重来——宁可重传一次,也别用一个坏包或坏模型去训练评测。
9.4 演练与排错
不想真等到训练结束才验证关机逻辑,可以用 DRY_RUN=1 演练:脚本会完整走一遍「等进程 → 等结束 → 延迟 → 该关机了」,但不真的执行关机。下面用 sleep 45 冒充训练进程,约 1 分钟出结果:
cd /root/autodl-tmp
sleep 45 &
sleep 1
PATTERN='sleep 45' WAIT_SECS=10 TAIL_SLEEP=2 DRY_RUN=1 bash auto_shutdown.sh四个环境变量都能临时覆盖,不必改脚本:
| 变量 | 默认 | 作用 |
|---|---|---|
PATTERN | lerobot-train | 要等的进程关键字 |
WAIT_SECS | 300 | 启动后最长等多久「训练出现」;超时就退出,绝不关机 |
TAIL_SLEEP | 60 | 训练结束后再等多久才关机(留给磁盘同步) |
DRY_RUN | 0 | 1 = 只打印不关机 |
按现象对号入座:
| 现象 | 原因 | 处理 |
|---|---|---|
deploy.sh 每个文件都要输密码 | 连接复用没建立 | 按脚本提示配公钥:ssh-copy-id -p 端口 root@主机 |
解压刷几百行 LIBARCHIVE.xattr… | 包是加 --no-xattrs 之前打的 | 不影响解压正确性(deploy.sh 已静音);想彻底干净就重新打包 |
setup.sh 报 torch 不是 2.8 / CUDA12.8 | 镜像不对 | 换 AutoDL 官方 PyTorch 2.8 镜像重来 |
setup.sh 末尾不是 episodes=50 frames=27260 | 数据集没传或传坏了 | 重跑 ./deploy.sh … -d(它会核对字节数) |
train_act_so101.sh 说没有 WANDB_API_KEY | 新终端没加载 ~/.bashrc | source ~/.bashrc 后重跑 |
| 守护挂上后一直不关机 | pgrep -f 命中了别的进程 | 见 9.3 第一条,清掉含 lerobot-train 的命令行 |
| 守护日志写「所有关机方式都没生效」 | 容器里没有 shutdown | 在 AutoDL 控制台手动关机 |
fetch 下载中断 | 网络波动 | 直接重跑,已完成的会自动跳过 |
附录:7 个脚本完整源码
以下是 autodl/ 下 7 个脚本的完整源码,可直接照抄复用。两点说明:
- 凭据已脱敏:
wandb_login.sh里的 key 替换成了占位符wandb_v1_你的key,使用前从 wandb.ai/authorize 获取并填入(注意该文件含明文 key,不要提交 git)。 - steps 数值:脚本当前
--steps=30000(对应 6 个 checkpoint:005000…030000);上一篇正文示例中的 25000(5 个 checkpoint)是某次实际运行的记录,两处按需修改即可,其余逻辑一致。
deploy.sh(Mac · 上传代码包与脚本,远端解压校验)
#!/usr/bin/env bash
# ./deploy.sh -h <地址> -P <端口> -u root [-d]
# -d 一并上传数据集 so101_demo.tar.gz
# 密码只输一次(SSH 连接复用); 想一次都不输: ssh-copy-id -p <端口> root@<地址>
# 每次运行都是完整重传: 不做断点续传, 也不跳过远端已有的同名文件。
# 打包 tar.gz 必须带 --no-xattrs, 否则远端 GNU tar 解包时每个文件刷一行 extended header 警告。
set -euo pipefail
HOST= PORT= USER= DATASET=0
while getopts "h:P:u:d" o; do case $o in
h) HOST=$OPTARG ;; P) PORT=$OPTARG ;; u) USER=$OPTARG ;;
d) DATASET=1 ;;
*) echo "用法: $0 -h <地址> -P <端口> -u root [-d]"; exit 1 ;;
esac; done
[ -n "$HOST" ] && [ -n "$PORT" ] && [ -n "$USER" ] || { echo "!! 缺少 -h/-P/-u"; exit 1; }
DIR=$(cd "$(dirname "$0")" && pwd)
REMOTE=/root/autodl-tmp # 必须绝对路径: 写成 ~ 会被本地 shell 展开成本机家目录
FILES=("$DIR/lerobot-main.tar.gz" "$DIR/setup.sh" "$DIR/train_act_so101.sh" "$DIR/auto_shutdown.sh")
[ -f "$DIR/wandb_login.sh" ] && FILES+=("$DIR/wandb_login.sh")
[ "$DATASET" = 1 ] && FILES+=("$DIR/so101_demo.tar.gz")
CTL_PATH="/tmp/lerobot_cm_%r@%h:%p"
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=15 -o ControlMaster=auto -o ControlPath=$CTL_PATH -o ControlPersist=600"
scp_try() {
# shellcheck disable=SC2086
scp $SSH_OPTS -P "$PORT" "$1" "$USER@$HOST:$REMOTE/$2"
}
ssh_try() {
# shellcheck disable=SC2086
ssh $SSH_OPTS -p "$PORT" "$USER@$HOST" "$1"
}
remote_size() { ssh_try "stat -c %s $REMOTE/$1 2>/dev/null" 2>/dev/null | grep -xE '[0-9]+' | tail -1 || true; }
put() {
local src="$1" name have total
name=$(basename "$src")
total=$(stat -f %z "$src" 2>/dev/null || stat -c %s "$src")
scp_try "$src" "$name" || { echo " !! $name 上传失败"; return 1; }
have=$(remote_size "$name")
if [ "${have:-0}" = "$total" ]; then
echo " ok $name ($total B)"
else
echo " !! $name 大小不一致 (远端 ${have:-?} / 本地 $total), 重跑一次"
return 1
fi
}
ssh_try true >/dev/null 2>&1 \
|| echo " !! 复用连接没建立, 后面每个文件都会重新要密码; 建议先配公钥: ssh-copy-id -p $PORT $USER@$HOST"
for f in "${FILES[@]}"; do put "$f" || exit 1; done
echo "解压..."
TARW=--warning=no-unknown-keyword
ssh_try "tar $TARW --version >/dev/null 2>&1" || TARW=
ssh_try "cd $REMOTE && tar $TARW -xzf lerobot-main.tar.gz && echo ' 代码包 ok'"
[ "$DATASET" = 1 ] && ssh_try "cd $REMOTE && tar $TARW -xzf so101_demo.tar.gz && echo ' 数据集 ok'"
echo
echo "下一步 (ssh 上去执行):"
echo " cd $REMOTE && bash setup.sh # 装环境"
echo " bash $REMOTE/train_act_so101.sh # 启动训练"
echo " setsid nohup bash $REMOTE/auto_shutdown.sh >> $REMOTE/auto_shutdown.log 2>&1 < /dev/null &"setup.sh(云端 · 8 步装环境并自检)
#!/usr/bin/env bash
# AutoDL 环境准备: 装 lerobot + 校验 torch/torchcodec + 配镜像 + 登 wandb + 自检数据集。
# 用法: bash setup.sh
# PIP_INDEX=<源> bash setup.sh # 换 pip 源, 不用改文件
set -u
SELF=$(cd "$(dirname "$0")" && pwd)
PROJ=/root/autodl-tmp/lerobot-main
DATASET=/root/autodl-tmp/so101_demo
CONDA=/root/miniconda3
PIP_INDEX="${PIP_INDEX:-https://mirrors.aliyun.com/pypi/simple/}"
echo "== 1. GPU =="
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader || { echo "!! 没检测到 GPU"; exit 1; }
echo "== 2. conda + 镜像源 =="
source "$CONDA/etc/profile.d/conda.sh" && conda activate base
# AutoDL 镜像预置的清华 pkgs/free 已下线(404), 必须移除
conda config --remove channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free 2>/dev/null
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main >/dev/null
pip config set global.index-url "$PIP_INDEX" >/dev/null
echo " pip 源: $PIP_INDEX"
python -V
echo "== 3. torch =="
python -c "import torch,sys; assert torch.__version__.startswith('2.8') and torch.version.cuda=='12.8', torch.__version__; print(' torch', torch.__version__, 'cuda', torch.version.cuda)" \
|| { echo "!! 镜像的 torch 不是 2.8/CUDA12.8, 请换 AutoDL 官方 PyTorch 2.8 镜像"; exit 1; }
echo "== 4. lerobot =="
cd "$PROJ"
if [ -x "$CONDA/bin/lerobot-train" ]; then
echo " 已安装, 跳过"
else
# 官方 0.6.x 有 [training] extra; 本仓库(0.4.x fork)没有, 训练依赖都在 base dependencies 里
EXTRAS=""
grep -qE '^training[[:space:]]*=' pyproject.toml && EXTRAS="[training]"
# uv 并发下载, 单连接慢的源上总时长常能压到 1/3。uv 装不上或失败都退回 pip。
command -v uv >/dev/null 2>&1 || pip install -q --index-url "$PIP_INDEX" uv
if ! { command -v uv >/dev/null 2>&1 \
&& uv pip install --index-url "$PIP_INDEX" --python "$CONDA/bin/python" -e ".$EXTRAS"; }; then
echo " uv 不可用或失败, 退回 pip"
pip install -e ".$EXTRAS"
fi
fi
echo "== 5. torchcodec (GPU 视频解码; 缺了会退成 CPU 软解, 慢 2 倍以上) =="
python -c "import torchcodec" 2>/dev/null || conda install -y -c conda-forge "ffmpeg<7"
python -c "import torchcodec; print(' torchcodec', torchcodec.__version__, '就绪')" 2>/dev/null \
|| echo " !! torchcodec 加载失败(通常缺 libnvrtc), 训练会回退 pyav 软解, 速度约减半"
echo "== 6. HF 镜像 (首次下载数据集/模型用) =="
# 写 ~/.bashrc 只对"以后新开的终端"生效; 必须同时 export, 本次运行才立刻生效
grep -qs '^export HF_ENDPOINT=' ~/.bashrc \
|| echo 'export HF_ENDPOINT=https://hf-mirror.com' >> ~/.bashrc
export HF_ENDPOINT=https://hf-mirror.com
echo " HF_ENDPOINT=$HF_ENDPOINT (本次会话已生效, 并已写入 ~/.bashrc)"
echo "== 7. wandb 登录 =="
[ -f "$SELF/wandb_login.sh" ] && source "$SELF/wandb_login.sh" || echo " 未见 wandb_login.sh, 跳过"
echo "== 8. 数据集自检 =="
if [ -d "$DATASET" ]; then
python - <<PY
from lerobot.datasets.lerobot_dataset import LeRobotDataset
ds = LeRobotDataset("local/so101_demo", root="$DATASET")
print(f" episodes={ds.meta.total_episodes} frames={ds.meta.total_frames} fps={ds.fps}")
print(f" 相机: {', '.join(ds.meta.camera_keys)}")
PY
else
echo " 未找到 $DATASET (没上传数据集?), 跳过"
fi
echo
echo "完成。启动训练: bash $SELF/train_act_so101.sh"
# export 只在当前进程树内有效: bash setup.sh 是子 shell, 变量回不到你的终端
if [ "${BASH_SOURCE[0]:-$0}" = "$0" ]; then
echo "注意: 本次以子 shell 运行, HF_ENDPOINT / WANDB_API_KEY 仅在本脚本内生效。"
echo " 当前终端要用它们, 请执行: source ~/.bashrc"
else
echo "本次以 source 方式运行, HF_ENDPOINT / WANDB_API_KEY 已在当前终端生效。"
fiwandb_login.sh(云端 · 导出 key 并写入 ~/.bashrc)
#!/usr/bin/env bash
# wandb 登录 —— 走 WANDB_API_KEY 环境变量这条路。
#
# 为什么不用 `wandb login`:
# 新版 key 是 86 字符的 wandb_v1_xxx, 而旧版 wandb SDK 的 login 会硬校验 40 字符,
# 直接抛 "API key must be 40 characters long" 而失败(实测)。用它写 ~/.netrc 走不通,
# 但 wandb.init() 自己会读 WANDB_API_KEY, 所以导出环境变量即可。
#
# 幂等, 可重复执行; 被 setup.sh source, 所以内部不调用 exit。
# 本文件含明文 key: 勿提交 git / 勿外发。轮换: https://wandb.ai/settings 改下一行即可。
export WANDB_API_KEY='wandb_v1_你的key'
if grep -qs '^export WANDB_API_KEY=' "$HOME/.bashrc"; then
sed -i "s|^export WANDB_API_KEY=.*|export WANDB_API_KEY='$WANDB_API_KEY'|" "$HOME/.bashrc"
else
echo "export WANDB_API_KEY='$WANDB_API_KEY'" >> "$HOME/.bashrc"
fi
echo " [OK] WANDB_API_KEY 已导出, 并写入 ~/.bashrc (新终端/重开机后依然有效)"train_act_so101.sh(云端 · 后台启动 ACT 训练)
#!/usr/bin/env bash
# 启动 ACT 训练。用法: bash train_act_so101.sh
# steps 固定 30000, 不按数据集帧数折算 epoch —— 换数据集后这里不用跟着改。
# 前置: 当前 shell 里已导出 WANDB_API_KEY(见 wandb_login.sh)
# 没有凭据时训练会立刻报 api_key not configured 退出, 所以下面先做检查
set -u
if [ -z "${WANDB_API_KEY:-}" ]; then
echo "!! 当前 shell 没有 WANDB_API_KEY, 训练会立刻失败"
echo " 先执行: export WANDB_API_KEY='wandb_v1_...' # 完整 key 见 wandb.ai/authorize"
echo " 建议同时写入 ~/.bashrc, 免得每次重开机都要再导一遍"
echo " 验证: echo \$WANDB_API_KEY # 应打印 wandb_v1_ 开头的串"
exit 1
fi
setsid nohup /root/miniconda3/bin/lerobot-train \
--dataset.repo_id=local/so101_demo \
--dataset.root=/root/autodl-tmp/so101_demo \
--policy.type=act \
--output_dir=/root/autodl-tmp/outputs/train/act_so101_demo \
--job_name=act_so101_demo \
--batch_size=32 \
--save_freq=5000 \
--num_workers=12 \
--policy.device=cuda \
--wandb.enable=true \
--policy.push_to_hub=false \
--steps=30000 \
> /root/autodl-tmp/so101_train.log 2>&1 < /dev/null &
# 确认真的起来了 —— 原来无论成功失败都打印「训练正在进行中」, 失败会被静默掩盖
sleep 5
if pgrep -f '[l]erobot-train' >/dev/null 2>&1; then
echo "训练已启动 (PID $(pgrep -f '[l]erobot-train' | head -1))"
echo "实时查看: tail -f /root/autodl-tmp/so101_train.log"
else
echo "!! 5 秒内没看到 lerobot-train 进程, 启动大概率失败了, 日志末尾:"
tail -20 /root/autodl-tmp/so101_train.log
exit 1
fiauto_shutdown.sh(云端 · 训练结束自动关机)
#!/usr/bin/env bash
# 等训练进程结束后自动关机, 避免训练跑完 GPU 一直空转计费。
#
# 【真跑】服务器上后台运行(日志追加到 /root/autodl-tmp/auto_shutdown.log):
# cd /root/autodl-tmp
# setsid nohup bash auto_shutdown.sh >> /root/autodl-tmp/auto_shutdown.log 2>&1 < /dev/null &
#
# 【演练】不真的关机, 约 1 分钟, 前台直接看到输出:
# sleep 45 &
# sleep 1
# PATTERN='sleep 45' WAIT_SECS=10 TAIL_SLEEP=2 DRY_RUN=1 bash auto_shutdown.sh
#
# 安全设计: 启动后最多等 WAIT_SECS 秒, 一直没发现训练在跑 -> 打印一行就退出, 绝不误关机。
set -u
PATTERN="${PATTERN:-lerobot-train}" # 进程关键字, 用 pgrep -f 匹配
WAIT_SECS="${WAIT_SECS:-300}" # 启动后最多等多久训练出现(秒)
TAIL_SLEEP="${TAIL_SLEEP:-60}" # 训练结束后延迟多少秒再关机(留给磁盘同步)
DRY_RUN="${DRY_RUN:-0}" # 1 = 只打印, 不真的关机
log() { echo "$(date '+%F %T') $*"; }
running() { pgrep -f -- "$PATTERN" >/dev/null 2>&1; }
# ---- 1) 等训练出现; 等不到就退出, 不关机 ----
waited=0
while [ "$waited" -lt "$WAIT_SECS" ]; do
running && break
sleep 10
waited=$((waited + 10))
done
if ! running; then
log "没发现 '$PATTERN' 在运行(等了 ${WAIT_SECS}s), 不关机, 退出"
exit 0
fi
log "检测到 '$PATTERN' 运行中, 开始守护(每 60s 检查一次)"
# ---- 2) 等它结束 ----
while running; do sleep 60; done
log "进程已结束, ${TAIL_SLEEP}s 后关机 (checkpoint 在数据盘, 重开机仍在)"
sleep "$TAIL_SLEEP"
sync
# ---- 3) 关机 ----
if [ "$DRY_RUN" = "1" ]; then
log "DRY_RUN=1, 本该执行关机命令, 这里不执行(演练结束)"
exit 0
fi
# AutoDL 官方文档给的是 /usr/bin/shutdown, 这里按优先级依次尝试。
# 只要有一个真的生效, 机器立刻断电, 后面的分支不会被执行。
for cmd in "/usr/bin/shutdown" "/usr/bin/shutdown -h now" "/usr/sbin/shutdown -h now" "/sbin/shutdown -h now"; do
bin="${cmd%% *}"
if [ -x "$bin" ]; then
log "执行关机: $cmd"
$cmd && exit 0
log " '$cmd' 返回非 0, 换下一种"
fi
done
log "所有关机方式都没生效, 请手动在 AutoDL 控制台关机"
exit 1check_remote.sh(Mac · 一条命令查全远端状态)
#!/usr/bin/env bash
# 一条命令查清服务器状态: checkpoint / 训练进程 / 日志尾部 / 自动关机守护。
# 输出同时显示在屏幕上, 并写入项目根目录的 _remote_check.txt (供 AI 直接读取)。
#
# 用法 (与 deploy.sh / fetch_checkpoints.sh 统一的 -h/-P/-u):
# ./check_remote.sh -h <地址> -P <端口> [-u root]
#
# 之所以写成一个脚本: 避免在命令行里手工拼一长串引号 (之前那串嵌套引号和 \r
# 反复出问题)。这里远端命令用单引号包住, 本地 shell 不做任何展开, 所见即所得。
set -u
HOST= PORT= USER=root
while getopts "h:P:u:" o; do case $o in
h) HOST=$OPTARG ;; P) PORT=$OPTARG ;; u) USER=$OPTARG ;;
*) echo "用法: $0 -h <地址> -P <端口> [-u root]"; exit 1 ;;
esac; done
shift $((OPTIND - 1))
# 兼容早期写法的位置参数: check_remote.sh [端口] [地址](已给 -P/-h 时不覆盖)
[ -n "${1:-}" ] && PORT="${PORT:-$1}"
[ -n "${2:-}" ] && HOST="${HOST:-$2}"
PORT="${PORT:-42143}"
HOST="${HOST:-connect.westd.seetacloud.com}"
OUT="$(cd "$(dirname "$0")/.." && pwd)/_remote_check.txt"
ssh -o StrictHostKeyChecking=no -p "$PORT" "$USER@$HOST" '
echo "=== 时间 ==="
date "+%F %T"
echo "=== checkpoint 列表 (steps=30000 / save_freq=5000, 应有 6 个: 005000 ... 030000) ==="
ls -1 /root/autodl-tmp/outputs/train/act_so101_demo/checkpoints/ 2>/dev/null || echo " 路径不存在"
echo "=== 训练进程 (无卡模式下应该为空) ==="
ps -eo pid,cmd | grep lerobot-train | grep -v grep || echo " 没有训练进程"
echo "=== 训练日志尾部 ==="
tail -c 1500 /root/autodl-tmp/so101_train.log 2>/dev/null | tr "\r" "\n" | tail -4
echo "=== 自动关机守护日志 ==="
cat /root/autodl-tmp/auto_shutdown.log 2>/dev/null || echo " 没有守护日志(说明守护从未启动)"
' 2>&1 | tee "$OUT"
echo
echo "已保存到: $OUT"
# 连不上时上面那段会全是空的, 中间那些「路径不存在 / 没有进程」都是假象, 这里直接点破
if grep -qE 'Connection refused|Connection timed out|Permission denied|Could not resolve' "$OUT"; then
echo
echo "!! 上面的 ssh 报错说明根本没连上, 中间那些「路径不存在/没有进程」都不算数。"
echo " Connection refused = 实例没开机, 或地址端口变了(每次开机都会换), 去控制台抄最新的「登录指令」。"
exit 1
fifetch_checkpoints.sh(Mac · 断点续传拉回模型并校验字节数)
#!/usr/bin/env bash
# 把服务器上所有 checkpoint 的 pretrained_model 拉到本地(models/ 下)。
# 评测只需要 pretrained_model(约 206MB/个), 不含 412MB 的续训状态。
# ./fetch_checkpoints.sh -h <地址> -P <端口> -u root [-p 密码] [-o 本地目录]
# 可重复执行: 已完成的会自动跳过, 中断后重跑即可(靠 .done 标记, 不是看文件是否存在)。
# 密码只需输入一次: 内置 SSH 连接复用(ControlMaster), 全程共用一条连接。
set -uo pipefail
HOST= PORT= USER= PASS=
REMOTE_OUT=/root/autodl-tmp/outputs/train/act_so101_demo
DEST="$(cd "$(dirname "$0")/.." && pwd)/models"
while getopts "h:P:u:p:o:" o; do case $o in
h) HOST=$OPTARG ;; P) PORT=$OPTARG ;; u) USER=$OPTARG ;;
p) PASS=$OPTARG ;; o) DEST=$OPTARG ;;
*) echo "用法: $0 -h <地址> -P <端口> -u root [-p 密码] [-o 本地目录]"; exit 1 ;;
esac; done
[ -n "$HOST" ] && [ -n "$PORT" ] && [ -n "$USER" ] || { echo "!! 缺少 -h/-P/-u"; exit 1; }
CTL_PATH="/tmp/lerobot_cm_%r@%h:%p"
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ControlMaster=auto -o ControlPath=$CTL_PATH -o ControlPersist=600"
ssh_do() {
if [ -n "$PASS" ]; then
PASS="$PASS" PORT="$PORT" HOST="$HOST" USER="$USER" CMD="$1" expect <<'EOF'
spawn ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ControlMaster=auto -o ControlPath=/tmp/lerobot_cm_%r@%h:%p -o ControlPersist=600 -p $env(PORT) $env(USER)@$env(HOST) $env(CMD)
expect { "yes/no" {send "yes\r"; exp_continue} "*assword:" {send "$env(PASS)\r"; exp_continue} eof }
catch wait r; exit [lindex $r 3]
EOF
else
# shellcheck disable=SC2086
ssh $SSH_OPTS -p "$PORT" "$USER@$HOST" "$1"
fi
}
scp_do() { # $1=远端路径 $2=本地目录
if [ -n "$PASS" ]; then
PASS="$PASS" PORT="$PORT" HOST="$HOST" USER="$USER" SRC="$1" DST="$2" expect <<'EOF'
spawn scp -r -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ControlMaster=auto -o ControlPath=/tmp/lerobot_cm_%r@%h:%p -o ControlPersist=600 -P $env(PORT) $env(USER)@$env(HOST):$env(SRC) $env(DST)
expect { "yes/no" {send "yes\r"; exp_continue} "*assword:" {send "$env(PASS)\r"; exp_continue} eof }
catch wait r; exit [lindex $r 3]
EOF
else
# shellcheck disable=SC2086
scp -r $SSH_OPTS -P "$PORT" "$USER@$HOST:$1" "$2"
fi
}
# 1) 先确认真的连上了: 连不上就退出, 否则「端口不通」会被后面的空列表伪装成「远端没有 checkpoint」
CONN=$(ssh_do "echo __ok" 2>&1)
case "$CONN" in
*__ok*) ;;
*)
echo "!! 连不上 $USER@$HOST:$PORT, ssh 的原话:"
printf '%s\n' "$CONN" | sed 's/^/ /'
echo " Connection refused = 实例没开机, 或端口已变(每次开机地址和端口都会换)"
echo " 到 AutoDL 控制台确认状态, 并抄最新的「登录指令」; 只验证连接可跑:"
echo " bash autodl/check_remote.sh -h $HOST -P $PORT"
exit 1
;;
esac
# ---- 2) 读 checkpoint 列表: 远端报错这次保留, 好让「目录不存在」看得见 ----
LIST=$(ssh_do "ls -1 $REMOTE_OUT/checkpoints/ 2>&1")
STEPS=$(printf '%s\n' "$LIST" | grep -xE '[0-9]+' | sort -n)
if [ -z "$STEPS" ]; then
echo "已连上, 但没读到 checkpoint, 远端原话:"
printf '%s\n' "$LIST" | sed 's/^/ /'
echo " (No such file = 训练还没跑到第 5000 步; 第一个 checkpoint 在 step 5000)"
exit 0
fi
echo "远端 checkpoint: $(printf '%s\n' "$STEPS" | tr '\n' ' ')"
mkdir -p "$DEST"
for s in $STEPS; do
# 用 .done 标记判断完成度: 只看文件存在会把「下到一半的半截文件」当成已完成而永久跳过
if [ -f "$DEST/$s/.done" ]; then
echo " 跳过 ${s}(本地已有)"; continue
fi
mkdir -p "$DEST/$s"
rm -rf "$DEST/$s/pretrained_model" # 清掉上次中断可能留下的半截目录
echo " 下载 $s ..."
scp_do "$REMOTE_OUT/checkpoints/$s/pretrained_model" "$DEST/$s/" \
|| { echo " !! $s 下载中断, 重跑本脚本即可(已完成的会跳过)"; exit 1; }
# 落到本地后再对一次字节数, 一致才打 .done
rsz=$(ssh_do "stat -c %s $REMOTE_OUT/checkpoints/$s/pretrained_model/model.safetensors" 2>/dev/null | grep -xE '[0-9]+' | tail -1)
lsz=$(stat -f %z "$DEST/$s/pretrained_model/model.safetensors" 2>/dev/null \
|| stat -c %s "$DEST/$s/pretrained_model/model.safetensors" 2>/dev/null)
if [ -n "$rsz" ] && [ "$rsz" = "$lsz" ]; then
touch "$DEST/$s/.done"
echo " ok $s ($lsz B)"
else
echo " !! $s 大小不一致 (远端 ${rsz:-?} / 本地 ${lsz:-?}), 已删掉, 重跑本脚本"
rm -rf "$DEST/$s/pretrained_model"
exit 1
fi
done
echo
echo "完成, 本地目录:"
du -sh "$DEST"/* 2>/dev/null
echo
echo "评测示例:"
echo " lerobot-record --policy.path=$DEST/<步数>/pretrained_model --policy.device=mps ..."
# 关闭复用连接(尽力而为, 失败不影响结果)
# shellcheck disable=SC2086
ssh $SSH_OPTS -O exit -p "$PORT" "$USER@$HOST" >/dev/null 2>&1 || true